/**
 * Copyright (c) 2018-2023, NXOS Development Team
 * SPDX-License-Identifier: Apache-2.0
 *
 * Contains: Virtio specification utilities codes
 *
 * Change Logs:
 * Date           Author            Notes
 * 2023-07-03     Shiroko           Init
 */
#include <nxos.h>

#ifdef CONFIG_NX_DRIVER_ENABLE_VIRTIO

#undef NX_LOG_MOD_NAME
#define NX_LOG_MOD_NAME "VIRTIO"

#include <base/log.h>

#include "virtio.h"
#include "devices/virtio_devices.h"

NX_PRIVATE VirtioDriverTableEntity *DriversTable[VIRTIO_DEVICE_TYPE_SIZE];
typedef VirtioDriverTableEntity *(*VirtioDriverTableEntityInitHandler)(void);
NX_PRIVATE VirtioDriverTableEntityInitHandler DriversTableEntityCreatorFuncs[] =
                                                      VIRTIO_DRIVERS_TABLE_ENTITY_CREATOR_FUNCS;

#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI

#include "virtio_pci.h"

#endif // CONFIG_NX_DRIVER_VIRTIO_PCI

#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO

#include "virtio_mmio.h"

#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO

// Common config
void VirtioCommonConfigGet(NX_Device *device, VirtioCommonConfigItem item, void *value)
{
    if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_PCI)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI
        VirtioPCICommonConfigGet(device, item, value);
#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
    }
    else if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_MMIO)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO
        VirtioMMIOCommonConfigGet(device, item, value);
#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO
    }
    NX_MemoryBarrier();
}

void VirtioCommonConfigSet(NX_Device *device, VirtioCommonConfigItem item, void *value)
{
    if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_PCI)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI
        VirtioPCICommonConfigSet(device, item, value);
#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
    }
    else if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_MMIO)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO
        VirtioMMIOCommonConfigSet(device, item, value);
#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO
    }
    NX_MemoryBarrier();
}

// Device Config
void *VirtioGetDeviceConfig(NX_Device *device)
{
    if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_PCI)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI
        return VirtioPCIGetDeviceConfig(device);
#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
    }
    else if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_MMIO)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO
        return VirtioMMIOGetDeviceConfig(device);
#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO
    }
    NX_PANIC("Unknown Virtio Bus Type.");
    return NX_NULL;
}

void VirtioReadDeviceConfig(NX_Device *device, NX_Offset offset, void *buf, NX_Size len)
{
    NX_U8 before, after;
    do
    {
        VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_CONFIG_GENERATION, &before);
        NX_MemCopy(buf, VirtioGetDeviceConfig(device) + offset, len);
        VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_CONFIG_GENERATION, &after);
    } while (after != before);
}

void VirtioWaitNotification(NX_Device *device, NX_U16 queue_index, NX_U32 desc_id)
{
    VirtioQueue *queue = VirtioQueueGet(device, queue_index);
    do
    {
        NX_ASSERT(NX_SemaphoreWait(&queue->trace[desc_id].lock) == NX_EOK);
    } while (!queue->trace[desc_id].ok);
    NX_ASSERT(NX_SemaphoreSignal(&queue->trace[desc_id].lock) == NX_EOK);
}

void VirtioSendNotification(NX_Device *device, NX_U16 queue_index, NX_U32 desc_id)
{
    if (desc_id != VIRTIO_QUEUE_DESC_INVALID_ID)
    {
        VirtioQueue *queue = VirtioQueueGet(device, queue_index);
        // Setup trace
        queue->trace[desc_id].waiting    = NX_True;
        queue->trace[desc_id].ok         = NX_False;
        queue->trace[desc_id].needNotify = NX_True;
        NX_ASSERT(NX_SemaphoreWait(&queue->trace[desc_id].lock) == NX_EOK);
    }

    if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_PCI)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI
        VirtioPCISendNotification(device, queue_index);
#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
    }
    else if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_MMIO)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO
        VirtioMMIOSendNotification(device, queue_index);
#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO
    }
}

// ISR
#ifdef CONFIG_NX_DRIVER_VIRTIO_USE_DELAY_QUEUE_FOR_ISR
#define VIRTIO_USE_DELAY_QUEUE 1
#else
#define VIRTIO_USE_DELAY_QUEUE 0
#endif // CONFIG_NX_DRIVER_VIRTIO_USE_DELAY_QUEUE_FOR_ISR

NX_INLINE void VirtioISRNotifyQueue(NX_Device *device, VirtioQueue *queue, NX_U32 idx)
{
    if (queue->trace[idx].waiting == NX_True)
    {
        // Someone is waiting for this trace
        queue->trace[idx].waiting = NX_False;
        queue->trace[idx].ok      = NX_True;
        NX_ASSERT(NX_SemaphoreSignal(&queue->trace[idx].lock) == NX_EOK);
    }
    if (queue->trace[idx].needNotify == NX_True && queue->notifier)
    {
        queue->trace[idx].needNotify = NX_False;
        NX_Error err;
        if ((err = queue->notifier(device, queue, idx)) != NX_EOK)
            NX_LOG_E("ISR Notifier failed: %s.", NX_ErrorToString(err));
    }
}

NX_PRIVATE void VirtioISRQueue(void *_device)
{
    NX_Device       *device          = _device;
    VirtioExtension *deviceExtension = VirtioGetExtension(device);

    // queue interrupt
    for (int i = 0; i < deviceExtension->queue_num; i++)
    {
        // Look through all virtqueue to check any progress has been made
        VirtioQueue *queue = &deviceExtension->queue[i];
        NX_UArch    level;
        NX_ASSERT(NX_SpinLockIRQ(&queue->usedRingLock, &level) == NX_EOK);
        NX_U32          *used_idx = &queue->used_idx;
        VirtioQueueUsed *used     = queue->used;

        /*
         * Note: If device send packets more than a ring_size, the things hang.
         */
        while ((*used_idx % queue->ring_size) != (used->idx % queue->ring_size))
        {
            NX_U32 idx = used->ring[*used_idx].id;
            *used_idx = (*used_idx + 1) % queue->ring_size;
            VirtioISRNotifyQueue(device, queue, idx);
        }
        NX_SpinUnlockIRQ(&queue->usedRingLock, level);
    }
}

NX_PRIVATE void VirtioISRConfig(void *_device)
{
    NX_Device       *device          = _device;
    VirtioExtension *deviceExtension = VirtioGetExtension(device);
    // config interrupt
    if (deviceExtension->configChangeNotifier)
        deviceExtension->configChangeNotifier(device);
}

NX_Error VirtioISR(VirtioISRType type, NX_Device *device)
{
#if VIRTIO_USE_DELAY_QUEUE
    VirtioExtension *deviceExtension = VirtioGetExtension(device);
#endif
    if (type == VIRTIO_ISR_TYPE_QUEUE)
    {
#if VIRTIO_USE_DELAY_QUEUE
        NX_IRQ_DelayWorkHandle(deviceExtension->irqWorkQueue);
#else
        VirtioISRQueue(device);
#endif
        return NX_EOK;
    }
    else if (type == VIRTIO_ISR_TYPE_CONFIG)
    {
#if VIRTIO_USE_DELAY_QUEUE
        NX_IRQ_DelayWorkHandle(deviceExtension->irqWorkConfig);
#else
        VirtioISRConfig(device);
#endif
        return NX_EOK;
    }
    return NX_EINVAL;
}

NX_Error VirtioSetupISR(NX_Device *device)
{
#if VIRTIO_USE_DELAY_QUEUE
    VirtioExtension *deviceExtension = VirtioGetExtension(device);
    if ((deviceExtension->irqWorkQueue  =
                 NX_IRQ_DelayWorkCreate(VirtioISRQueue, (void *) device, NX_IRQ_FAST_QUEUE)) == NX_NULL)
        return NX_EFAULT;
    if ((deviceExtension->irqWorkConfig =
                 NX_IRQ_DelayWorkCreate(VirtioISRConfig, (void *) device, NX_IRQ_FAST_QUEUE)) == NX_NULL)
    {
        NX_IRQ_DelayWorkDestroy(deviceExtension->irqWorkQueue);
        return NX_EFAULT;
    }

    if (NX_IRQ_DelayQueueEnter(NX_IRQ_FAST_QUEUE, deviceExtension->irqWorkQueue) != NX_EOK)
    {
        goto fault;
    }
    if (NX_IRQ_DelayQueueEnter(NX_IRQ_FAST_QUEUE, deviceExtension->irqWorkConfig) != NX_EOK)
    {
        NX_IRQ_DelayQueueLeave(NX_IRQ_FAST_QUEUE, deviceExtension->irqWorkQueue);
fault:
        NX_IRQ_DelayWorkDestroy(deviceExtension->irqWorkQueue);
        NX_IRQ_DelayWorkDestroy(deviceExtension->irqWorkConfig);
        return NX_EFAULT;
    }
#endif
    return NX_EOK;
}

NX_Error VirtioRemoveISR(NX_Device *device)
{
#if VIRTIO_USE_DELAY_QUEUE
    VirtioExtension *deviceExtension = VirtioGetExtension(device);
    NX_IRQ_DelayQueueLeave(NX_IRQ_FAST_QUEUE, deviceExtension->irqWorkQueue);
    NX_IRQ_DelayQueueLeave(NX_IRQ_FAST_QUEUE, deviceExtension->irqWorkConfig);

    NX_IRQ_DelayWorkDestroy(deviceExtension->irqWorkQueue);
    NX_IRQ_DelayWorkDestroy(deviceExtension->irqWorkConfig);
#endif
    return NX_EOK;
}

// Generic Initialization
NX_Error VirtioDeviceInitialize(NX_Device *device, NX_U32 *driverFeatures, NX_Size driverFeaturesCount)
{
    // Acknowledge device
    NX_U8 status = VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER;
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    VirtioFeature features;
    // Negotiate device supported features
    for (int      i   = 0; i < driverFeaturesCount; i++)
    {
        features.selector = i;
        VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_FEATURE_DEVICE, &features);
        features.features &= driverFeatures[i];
        VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_FEATURE_DRIVER, &features);
    }
    // Disable not supported features
    features.selector = 0;
    VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_FEATURE_DEVICE, &features);
    features.features &= ~(VIRTIO_FEATURE_EVENT_IDX | VIRTIO_FEATURE_INDIRECT_DESC | VIRTIO_FEATURE_ANY_LAYOUT);
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_FEATURE_DRIVER, &features);

    // Features OK
    VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    status |= VIRTIO_STATUS_FEATURES_OK;
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    if (status & VIRTIO_STATUS_FEATURES_OK)
    {
        // Great, device accepted our features set
        return NX_EOK;
    }
    else
    {
        // Device did NOT accept our features set
        return NX_EINVAL;
    }
}

NX_Error VirtioDeviceDriverOK(NX_Device *device)
{
    NX_U8 status = 0;
    VirtioCommonConfigGet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    status |= VIRTIO_STATUS_DRIVER_OK;
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    return NX_EOK;
}

NX_Error VirtioDeviceReset(NX_Device *device)
{
    NX_U8 status = 0;
    VirtioCommonConfigSet(device, VIRTIO_COMMON_CFG_DEVICE_STATUS, &status);
    return NX_EOK;
}

void VirtioCleanUpDevice(NX_Device *device)
{
    if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_PCI)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI
        VirtioPCICleanUpDevice(device);
#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
    }
    else if (VirtioGetExtension(device)->busType == VIRTIO_BUS_TYPE_MMIO)
    {
#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO
        VirtioMMIOCleanUpDevice(device);
#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO
    }
}

NX_PRIVATE void VirtioDriverInit(void)
{
    /*
     * Create drivers table. NXOS have no precise init order control.
     * I don't want to populate link-script either.
     */
    NX_MemSet(DriversTable, 0, sizeof(VirtioDriverTableEntity *) * VIRTIO_DEVICE_TYPE_SIZE);
    // Enumerate virtio drivers and create DriverTable
    for (int i = 0; i < sizeof(DriversTableEntityCreatorFuncs) / sizeof(VirtioDriverTableEntityInitHandler); i++)
    {
        VirtioDriverTableEntity *entity = DriversTableEntityCreatorFuncs[i]();
        if (entity == NX_NULL) continue; // dummy driver
        NX_ASSERT(entity->deviceId != 0 && entity->deviceId < VIRTIO_DEVICE_TYPE_SIZE);
        NX_ASSERT(entity->driverName != NX_NULL);
        NX_ASSERT(entity->deviceNameTemplate != NX_NULL);
        NX_ASSERT(entity->deviceType != NX_DEVICE_TYPE_UNKNOWN);
        NX_ASSERT(entity->driverOps != NX_NULL);
        NX_ASSERT(entity->initHandler != NX_NULL);
        NX_ASSERT(DriversTable[entity->deviceId] == NX_NULL);
        DriversTable[entity->deviceId] = entity;
        NX_Driver *driver = NX_DriverCreate(entity->driverName, entity->deviceType, 0, entity->driverOps);
        if (driver == NX_NULL)
        {
            NX_LOG_W("Create driver failed for %s.", entity->driverName);
            DriversTable[entity->deviceId] = NX_NULL;
            continue;
        }
        entity->driver      = driver;
        entity->deviceCount = 0;
        NX_ListInit(&entity->deviceListHead);
        NX_LOG_D("Create driver successfully for %s.", entity->driverName);
    }

#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI
    NX_ASSERT(VirtioPCISetup(DriversTable) == NX_EOK);
#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
#ifdef CONFIG_NX_DRIVER_VIRTIO_MMIO
    // MMIO Device Info should be passed through kernel parameters or device tree.
    NX_ASSERT(VirtioMMIOSetup(DriversTable) == NX_EOK);
#endif // CONFIG_NX_DRIVER_VIRTIO_MMIO

    // Register drivers
    for (int i = 0; i < VIRTIO_DEVICE_TYPE_SIZE; i++)
    {
        VirtioDriverTableEntity *entity = DriversTable[i];
        if (entity)
        {
            NX_ASSERT(entity->driver != NX_NULL);
            if (NX_DriverRegister(entity->driver) != NX_EOK)
            {
                NX_LOG_E("Register driver %s failed!", entity->driverName);
                // Detach device if ever created
                if (entity->deviceCount != 0)
                {
                    VirtioExtension *deviceExtension;
                    NX_ListForEachEntry(deviceExtension, &entity->deviceListHead, list)
                    {
                        if (NX_DriverDetachDevice(entity->driver, deviceExtension->device->name) != NX_EOK)
                        {
                            NX_LOG_E("Detach device %s form driver %s failed!",
                                     deviceExtension->device->name,
                                     entity->driverName);
                        }
                    }
                }
                NX_DriverDestroy(entity->driver);
            }
        }
    }
}

NX_PRIVATE void VirtioDriverExit(void)
{
    for (int i = 0; i < VIRTIO_DEVICE_TYPE_SIZE; i++)
    {
        VirtioDriverTableEntity *entity = DriversTable[i];
        if (entity)
        {
            NX_ASSERT(entity->driver != NX_NULL);
            // Detach device
            if (entity->deviceCount != 0)
            {
                VirtioExtension *deviceExtension;
                NX_ListForEachEntry(deviceExtension, &entity->deviceListHead, list)
                {
                    NX_Device *device = deviceExtension->device;
                    if (entity->cleanUpHandler)
                        entity->cleanUpHandler(device);
                    VirtioCleanUpDevice(device);
                }
            }
            NX_DriverDestroy(entity->driver);
        }
    }

}

NX_DRV_INIT(VirtioDriverInit);
NX_DRV_EXIT(VirtioDriverExit);

#endif // CONFIG_NX_DRIVER_ENABLE_VIRTIO
